/**
 * @author nttdocomo
 */
define(function(require) {
	require('../component');
	require('../util/bindable');
	require('../selection/dataViewModel');
	aries.klass("aries.view.AbstractView", aries.Component.extend({
		triggerEvent : 'itemclick',
		triggerCtEvent : 'containerclick',

		/**
		 * @cfg {Boolean} preserveScrollOnRefresh=false
		 * True to preserve scroll position across refresh operations.
		 */
		preserveScrollOnRefresh : false,

		afterRender : function() {
			aries.Component.prototype.afterRender.apply(this, arguments);

			// Init the SelectionModel after any on('render') listeners have been added.
			// Drag plugins create a DragDrop instance in a render listener, and that needs
			// to see an itemmousedown event first.
			this.getSelectionModel().bindComponent(this);
		},

		/**
		 * Changes the data store bound to this view and refreshes it.
		 * @param {Ext.data.Store} store The store to bind to this view
		 */
		bindStore : function(store, initial) {
			var me = this;
			me.mixins.bindable.prototype.bindStore.apply(me, arguments);
			// Bind the store to our selection model
			me.getSelectionModel().bindStore(me.store);

			// If we have already achieved our first layout, refresh immediately.
			// If we have bound to the Store before the first layout, then onBoxReady will
			// call doFirstRefresh1
			if (me.componentLayoutCounter) {
				me.doFirstRefresh(store);
			}
		},

		/**
		 * <p>Function which can be overridden which returns the data object passed to this
		 * DataView's {@link #tpl template} to render the whole DataView.</p>
		 * <p>This is usually an Array of data objects, each element of which is processed by an
		 * {@link Ext.XTemplate XTemplate} which uses <tt>'&lt;tpl for="."&gt;'</tt> to iterate over its supplied
		 * data object as an Array. However, <i>named</i> properties may be placed into the data object to
		 * provide non-repeating data such as headings, totals etc.</p>
		 * @param {Ext.data.Model[]} records An Array of {@link Ext.data.Model}s to be rendered into the DataView.
		 * @param {Number} startIndex the index number of the Record being prepared for rendering.
		 * @return {Object[]} An Array of data objects to be processed by a repeating XTemplate. May also
		 * contain <i>named</i> properties.
		 */
		collectData : function(records, startIndex) {
			var data = [], i = 0, len = records.length, record;

			for (; i < len; i++) {
				record = records[i];
				data[i] = this.prepareData(record.attributes, startIndex + i, record);
			}
			return data;
		},

		/**
		 * @private
		 * Perform the first refresh of the View from a newly bound store.
		 *
		 * This is called when this View has been sized for the first time.
		 */
		doFirstRefresh : function(store) {
			var me = this;

			// 4.1.0: If we have a store, and the Store is *NOT* already loading (a refresh is on the way), then
			// on first layout, refresh regardless of record count.
			// Template may contain boilerplate HTML outside of record iteration loop.
			// Also, emptyText is appended by the refresh method.
			// We call refresh on a defer if this is the initial call, and we are configured to defer the initial refresh.
			if (store && !store.loading) {
				if (me.deferInitialRefresh) {
					if (!me.isDestroyed) {
						me.refresh();
					}
				} else {
					me.refresh();
				}
			}
		},

		/**
		 * Returns the store associated with this DataView.
		 * @return {Ext.data.Store} The store
		 */
		getStore : function() {
			return this.store;
		},
		// private
		initComponent : function() {
			var me = this, /*isDef = Ext.isDefined,*/ itemTpl = me.itemTpl, memberFn = {};

			if (itemTpl) {
				if ($.isArray(itemTpl)) {
					// string array
					itemTpl = itemTpl.join('');
				} else if (Ext.isObject(itemTpl)) {
					// tpl instance
					memberFn = $.extend(memberFn, itemTpl.initialConfig);
					itemTpl = itemTpl.html;
				}

				if (!me.itemSelector) {
					me.itemSelector = '.' + me.itemCls;
				}

				itemTpl = Ext.String.format('<tpl for="."><div class="{0}">{1}</div></tpl>', me.itemCls, itemTpl);
				me.tpl = _.template(itemTpl, memberFn);
			}

			//<debug>
			if (_.isUndefined(me.tpl) || _.isUndefined(me.itemSelector)) {
				Ext.Error.raise({
					sourceClass : 'Ext.view.View',
					tpl : me.tpl,
					itemSelector : me.itemSelector,
					msg : "DataView requires both tpl and itemSelector configurations to be defined."
				});
			}
			//</debug>

			aries.Component.prototype.initComponent.apply(this, arguments);
			if (_.isString(me.tpl) || $.isArray(me.tpl)) {
				me.tpl = me.tpl;
			}

			//<debug>
			// backwards compat alias for overClass/selectedClass
			// TODO: Consider support for overCls generation Ext.Component config
			if (!_.isUndefined(me.overCls) || !_.isUndefined(me.overClass)) {
				if (!_.isUndefined(Ext.global.console)) {
					Ext.global.console.warn('Ext.view.View: Using the deprecated overCls or overClass configuration. Use overItemCls instead.');
				}
				me.overItemCls = me.overCls || me.overClass;
				delete me.overCls;
				delete me.overClass;
			}

			if (me.overItemCls) {
				me.trackOver = true;
			}

			if (!_.isUndefined(me.selectedCls) || !_.isUndefined(me.selectedClass)) {
				if (!_.isUndefined(Ext.global.console)) {
					Ext.global.console.warn('Ext.view.View: Using the deprecated selectedCls or selectedClass configuration. Use selectedItemCls instead.');
				}
				me.selectedItemCls = me.selectedCls || me.selectedClass;
				delete me.selectedCls;
				delete me.selectedClass;
			}
			//</debug>

			//me.addEvents(
			/**
			 * @event beforerefresh
			 * Fires before the view is refreshed
			 * @param {Ext.view.View} this The DataView object
			 */
			//'beforerefresh',
			/**
			 * @event refresh
			 * Fires when the view is refreshed
			 * @param {Ext.view.View} this The DataView object
			 */
			//'refresh',
			/**
			 * @event viewready
			 * Fires when the View's item elements representing Store items has been rendered. If the {@link #deferInitialRefresh} flag
			 * was set (and it is <code>true</code> by default), this will be <b>after</b> initial render, and no items will be available
			 * for selection until this event fires.
			 * @param {Ext.view.View} this
			 */
			//'viewready',
			/**
			 * @event itemupdate
			 * Fires when the node associated with an individual record is updated
			 * @param {Ext.data.Model} record The model instance
			 * @param {Number} index The index of the record/node
			 * @param {HTMLElement} node The node that has just been updated
			 */
			//'itemupdate',
			/**
			 * @event itemadd
			 * Fires when the nodes associated with an recordset have been added to the underlying store
			 * @param {Ext.data.Model[]} records The model instance
			 * @param {Number} index The index at which the set of record/nodes starts
			 * @param {HTMLElement[]} node The node that has just been updated
			 */
			//'itemadd',
			/**
			 * @event itemremove
			 * Fires when the node associated with an individual record is removed
			 * @param {Ext.data.Model} record The model instance
			 * @param {Number} index The index of the record/node
			 */
			//'itemremove');

			//me.addCmpEvents();

			// Look up the configured Store. If none configured, use the fieldless, empty Store defined in Ext.data.Store.
			/*me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');*/
			me.bindStore(me.store, true);
			me.all = $();

			// We track the scroll position
			me.scrollState = {
				top : 0,
				left : 0
			};
			me.on({
				scroll : me.onViewScroll/*,
				 element : 'el',
				 scope : me*/
			});
		},

		/**
		 * Gets the selection model for this view.
		 * @return {Ext.selection.Model} The selection model
		 */
		getSelectionModel : function() {
			var me = this, mode = 'SINGLE';

			if (!me.selModel) {
				me.selModel = {};
			}

			if (me.simpleSelect) {
				mode = 'SIMPLE';
			} else if (me.multiSelect) {
				mode = 'MULTI';
			}

			$.extend(me.selModel, {
				allowDeselect : me.allowDeselect,
				mode : mode
			});

			if (!(me.selModel instanceof aries.selection.Model)) {
				me.selModel = new aries.selection.DataViewModel(me.selModel);
			}

			if (!me.selModel.hasRelaySetup) {
				me.relayEvents(me.selModel, ['selectionchange', 'beforeselect', 'beforedeselect', 'select', 'deselect', 'focuschange']);
				me.selModel.hasRelaySetup = true;
			}

			// lock the selection model if user
			// has disabled selection
			if (me.disableSelection) {
				me.selModel.locked = true;
			}

			return me.selModel;
		},

		getItemSelector : function() {
			return this.itemSelector;
		},

		/**
		 * Gets a template node.
		 * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node,
		 * the id of a template node or the record associated with the node.
		 * @return {HTMLElement} The node or null if it wasn't found
		 */
		getNode : function(nodeInfo) {
			if ((!nodeInfo && nodeInfo !== 0) || !this.rendered) {
				return null;
			}

			if (_.isString(nodeInfo)) {
				return document.getElementById(nodeInfo);
			}
			if (_.isNumber(nodeInfo)) {
				return this.all.eq(nodeInfo);
			}
			if ( nodeInfo instanceof Backbone.Model) {
				return this.getNodeByRecord(nodeInfo);
			}
			return nodeInfo;
			// already an HTMLElement
		},

		/**
		 * @private
		 */
		getNodeByRecord : function(record) {
			var ns = this.all, ln = ns.length, i = 0;

			for (; i < ln; i++) {
				if (ns.eq(i).attr("data-cid") === record.cid) {
					return ns.eq(i);
				}
			}

			return null;
		},

		/**
		 * Gets a record from a node
		 * @param {Ext.Element/HTMLElement} node The node to evaluate
		 *
		 * @return {Ext.data.Model} record The {@link Ext.data.Model} object
		 */
		getRecord : function(node) {
			return this.store.at(this.all.index(node));
		},

		getStoreListeners : function() {
			var me = this;
			return {
				'refresh' : me.onDataRefresh,
				'add' : me.onAdd,
				'remove' : me.onRemove,
				'update' : me.onUpdate,
				'clear' : me.refresh
			};
		},

		/**
		 * Finds the index of the passed node.
		 * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node, the id of a template node
		 * or a record associated with a node.
		 * @return {Number} The index of the node or -1
		 */
		indexOf : function(node) {
			//node = this.getNode(node);
			if (_.isNumber(node.viewIndex)) {
				return node.viewIndex;
			}
			return this.all.index(node);
		},

		onBindStore : function(store) {
			this.setMaskBind(store);
		},

		onBoxReady : function() {
			var me = this, store = me.store;

			aries.Component.prototype.onBoxReady.apply(this, arguments);
			me.doFirstRefresh(me.store);
		},

		// invoked by the selection model to maintain visual UI cues
		onItemDeselect : function(record) {
			var node = this.getNode(record);

			if (node) {
				node.removeClass(this.selectedItemCls);
			}
		},

		// invoked by the selection model to maintain visual UI cues
		onItemSelect : function(record) {
			var node = this.getNode(record);

			if (node) {
				node.addClass(this.selectedItemCls);
			}
		},

		/**
		 * Function which can be overridden to provide custom formatting for each Record that is used by this
		 * DataView's {@link #tpl template} to render each node.
		 * @param {Object/Object[]} data The raw data object that was used to create the Record.
		 * @param {Number} recordIndex the index number of the Record being prepared for rendering.
		 * @param {Ext.data.Model} record The Record being prepared for rendering.
		 * @return {Array/Object} The formatted data in a format expected by the internal {@link #tpl template}'s overwrite() method.
		 * (either an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}))
		 */
		prepareData : function(data, index, record) {
			var associatedData, attr;
			/*if (record) {
			 associatedData = record.getAssociatedData();
			 for (attr in associatedData) {
			 if (associatedData.hasOwnProperty(attr)) {
			 data[attr] = associatedData[attr];
			 }
			 }
			 }*/
			return data;
		},

		/**
		 * Refreshes the view by reloading the data from the store and re-rendering the template.
		 */
		refresh : function() {
			var me = this, targetEl, targetParent, oldDisplay, nextSibling, dom, records;

			if (!me.rendered || me.isDestroyed) {
				return;
			}

			if (!me.events.beforerefresh || me.trigger('beforerefresh', me) !== false) {
				targetEl = me.getTargetEl();
				records = me.store.getRange();
				dom = targetEl;

				// Updating is much quicker if done when the targetEl is detached from the document, and not displayed.
				// But this resets the scroll position, so when preserving scroll position, this cannot be done.
				if (!me.preserveScrollOnRefresh) {
					targetParent = dom.parent();
					oldDisplay = dom.css("display");
					//style.display;
					dom.hide();
					nextSibling = dom.next();
					dom.detach();
					//targetParent.removeChild(dom);
				}

				if (me.refreshCounter) {
					me.clearViewEl();
				} else {
					me.fixedNodes = targetEl.children().length;
					me.refreshCounter = 1;
				}

				// Always attempt to create the required markup after the fixedNodes.
				// Usually, for an empty record set, this would be blank, but when the Template
				// Creates markup outside of the record loop, this must still be honoured even if there are no
				// records.
				targetEl.append(_.template(me.tpl, {
					data : me.collectData(records, 0)
				}));
				//me.tpl.append(targetEl, me.collectData(records, 0));

				// The emptyText is now appended to the View's element
				// after any fixedNodes.
				if (records.length < 1) {
					if (!me.deferEmptyText || me.hasSkippedEmptyText) {
						Ext.core.DomHelper.insertHtml('beforeEnd', targetEl.dom, me.emptyText);
					}
					me.all.clear();
				} else {
					me.all = me.all.add(me.getItemSelector(), targetEl);
					me.updateIndexes(0);
				}

				me.selModel.refresh();
				me.hasSkippedEmptyText = true;

				if (!me.preserveScrollOnRefresh) {
					if (nextSibling.length) {
						dom.insertBefore(nextSibling);
					} else {
						targetParent.append(dom);
					}
					dom.css("display", oldDisplay);
				}

				// Ensure layout system knows about new content size
				this.refreshSize();

				me.trigger('refresh', me);

				// Upon first refresh, fire the viewready event.
				// Reconfiguring the grid "renews" this event.
				if (!me.viewReady) {
					// Fire an event when deferred content becomes available.
					// This supports grid Panel's deferRowRender capability
					me.viewReady = true;
					me.trigger('viewready', me);
				}
			}
		},

		/**
		 * @private
		 * Called by the framework when the view is refreshed, or when rows are added or deleted.
		 *
		 * These operations may cause the view's dimensions to change, and if the owning container
		 * is shrinkwrapping this view, then the layout must be updated to accommodate these new dimensions.
		 */
		refreshSize : function() {
			/*var sizeModel = this.getSizeModel();
			 if (sizeModel.height.shrinkWrap || sizeModel.width.shrinkWrap) {
			 this.updateLayout();
			 }*/
		},

		setMaskBind : function(store) {
			var mask = this.loadMask;
			if (mask && mask.bindStore) {
				mask.bindStore(store);
			}
		},

		// private
		updateIndexes : function(startIndex, endIndex) {
			var ns = this.all, records = this.store.getRange(), i;

			startIndex = startIndex || 0;
			endIndex = endIndex || ((endIndex === 0) ? 0 : (ns.length - 1));
			for ( i = startIndex; i <= endIndex; i++) {
				ns[i].viewIndex = i;
				ns[i].viewRecordId = records[i].internalId;
				if (!ns[i].boundView) {
					ns[i].boundView = this.id;
				}
			}
		}
	})).mixin({
		bindable : aries.util.Bindable
	})
})
